package ebnf.ebnfScannerParser;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;

import ebnf.ebnfGrammar.Expression;
import ebnf.ebnfGrammar.NonTerminal;

/**
 * Parser for EBNF.
 * 
 * @author Bart Wentink & Sander Meijer
 */
public class EBNFParser implements EBNFTokenKinds {

	private EBNFScanner scanner;
	private EBNFToken lookAhead;
	private String parsed;
	
	private ArrayList<NonTerminal> dataStructure;
	private NonTerminal currentNonTerminal;
	private Expression currentExpression;
	private int nestedCounter = 0;
	
	private boolean SHOW_PARSE_STEPS = false;
	

	/**
	 * Constructor
	 * 
	 */
	public EBNFParser(Boolean debugging) {
		SHOW_PARSE_STEPS = debugging;
	}

	/**
	 * Parses the given filename's file.
	 * @param {@link String} pathname 
	 * @return {@link String} parsed
	 */
	public String parse(String pathname) {
		parsed = "";
		dataStructure = new ArrayList<NonTerminal>();
		nestedCounter = 0;
		try {
			FileInputStream fis = new FileInputStream(new File(pathname));
			this.scanner = new EBNFScanner(new BufferedReader(
					new InputStreamReader(fis)));
			
			// Initialize lookAhead by getting the first token from the parser
			acceptIt();
			if (SHOW_PARSE_STEPS) {
				System.out.println("Commence parsing");
			}
	
			parse_EBNF();
			accept("root", EOF);
			
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		return parsed;
	}

	/**
	 * Throws a run time error when there is a error during the parsing.
	 * 
	 * @param {@link String} nonterminal
	 * @param {@link String} message
	 */
	private void parseError(String nonterminal, String message) {
		System.err.println();
		System.err.println("parse error in <" + nonterminal + "> : " + message);
		System.err.print("lookahead '" + lookAhead.getText() + "' ");
		System.err.print("(kind=" + lookAhead.getKind() + ") ");
		System.err.print("at " + lookAhead.getLine() + "/"
				+ lookAhead.getColumn());
		System.err.println();
		throw new RuntimeException();
	}

	/**
	 * accepts a token by putting the next token on lookAhead. </br>
	 */
	private void acceptIt() {
		try {
			lookAhead = scanner.nextToken();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException();
		}
	}

	/**
	 * Checks if the current token can be accepted, if not throws an error.
	 * 
	 * @param {@link String} nonterminal
	 * @param {@link Integer} kind
	 */
	private void accept(String nonterminal, int kind) {

		if (lookAhead.getKind() != kind) {
			parseError(nonterminal, "kind=" + kind + " expected");
		} else {
			parsed += (lookAhead.getText() + " ");
			acceptIt();
		}
	}

	/**
	 * m_ass_seq EOBNF
	 */
	private void parse_EBNF() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse EBNF");
		}
		
		parse_m_ass_seq();
		accept(lookAhead.getText(), EOBNF);
	}

	/**
	 * m_ass m_ass_seq_assist
	 */
	private void parse_m_ass_seq() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_ass_seq");
		}
		
		parse_m_ass();
		parse_m_ass_seq_assist();
	}

	/**
	 * m_ass m_ass_seq_assist | @ 
	 */
	private void parse_m_ass_seq_assist() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_ass_seq_assist 1");
			System.out.println("kind: " + lookAhead.getKind());
		}
		
		if (lookAhead.getKind() != EOBNF) {
			if (SHOW_PARSE_STEPS) {
				System.out.println("kind: " + lookAhead.getKind());
				System.out.println("parse m_ass_seq_assist 2");
			}
			
			parse_m_ass();
			parse_m_ass_seq_assist();
		}
	}

	/**
	 * m_var ISDEFINEDAS m_expr EOR ;
	 */
	private void parse_m_ass() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_ass");
		}
		currentNonTerminal = getNonTerminal(lookAhead);
		currentExpression = new Expression();
		currentNonTerminal.addExpression(currentExpression);
		
		parse_m_var();
		accept(lookAhead.getText(), ISDEFINEDAS);
		parse_m_expr();
		
		accept(lookAhead.getText(), EOR);
		parsed += "\n";
	}

	/**
	 * m_term m_assist_expr | @ ;
	 */
	private void parse_m_expr() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_expr 1");
			System.out.println(lookAhead.getKind());
		}
		
		if (lookAhead.getKind() != EOR && lookAhead.getKind() != RP) {
			if (SHOW_PARSE_STEPS) {
				System.out.println("parse m_expr 2");
			}
			
			parse_m_term();
			parse_m_assist_expr();
		}
	}

	/**
	 * OR m_term m_assist_expr | @ ;
	 */
	private void parse_m_assist_expr() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_assist_expr 1");
		}
		
		if (lookAhead.getKind() == OR) {
			if (SHOW_PARSE_STEPS) {
				System.out.println("parse m_assist_expr 2");
			}
			
			accept(lookAhead.getText(), OR);
			currentExpression = new Expression();
			currentNonTerminal.addExpression(currentExpression);
			
			parse_m_term();
			parse_m_assist_expr();
		}
	}

	/**
	 * m_factor m_assist_term ;
	 */
	private void parse_m_term() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_term ");
		}
		
		parse_m_factor();
		parse_m_assist_term();
	}

	/**
	 * m_factor m_assist_term | @ ;
	 */
	private void parse_m_assist_term() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_assist_term 1");
			System.out.println(lookAhead.getKind());
		}
		
		if (lookAhead.getKind() != EOR && lookAhead.getKind() != OR
				&& lookAhead.getKind() != RP && lookAhead.getKind() != EMPTY) {
			if (SHOW_PARSE_STEPS) {
				System.out.println("parse m_assist_term 2");
				System.out.println(lookAhead.getKind());
			}
			
			parse_m_factor();
			parse_m_assist_term();
		}
	}

	/**
	 * IDENTIFIER | LP m_expr RP | CONSTANT | EMPTY ;
	 */
	private void parse_m_factor() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_factor 1");
		}
		
		switch (lookAhead.getKind()) {
		case IDENTIFIER:
			if (SHOW_PARSE_STEPS) {
				System.out.println("parse m_factor 2 IDENTIFIER");
			}
			currentExpression.addToken(lookAhead);
			accept(lookAhead.getText(), IDENTIFIER);
			break;
		case CONSTANT:
			if (SHOW_PARSE_STEPS) {
				System.out.println("parse m_factor 2 CONSTANT");
				System.out.println("mfactor " + lookAhead.getText());
			}
			String tempString = lookAhead.getText().substring(1, 2);
			EBNFToken token = new EBNFToken(lookAhead.getKind(), lookAhead.getLine(), 
									lookAhead.getColumn(), tempString);
			currentExpression.addToken(token);
			accept(lookAhead.getText(), CONSTANT);
			break;
		case LP:
			if (SHOW_PARSE_STEPS) {
				System.out.println("parse m_factor 2 LP");
				System.out.println("mfactor " + lookAhead.getText());
			}
			nestedCounter++;
			accept(lookAhead.getText(), LP);
			
			
			EBNFToken tempToken = new EBNFToken(1337, lookAhead.getLine(),lookAhead.getColumn(), ("nestednonterm_" + nestedCounter));
			currentExpression.addToken(tempToken);
			
			NonTerminal tempTerminal = currentNonTerminal;
			Expression tempExpression = currentExpression;
					
			currentNonTerminal = new NonTerminal(tempToken );
			currentExpression = new Expression();
			currentNonTerminal.addExpression(currentExpression);
			
			parse_m_expr();
			
			
			dataStructure.add(currentNonTerminal);
			
			currentNonTerminal = tempTerminal;
			currentExpression  = tempExpression;
			
			tempTerminal   = null;
			tempExpression = null;
			
//			currentExpression.addToken(lookAhead);
			accept(lookAhead.getText(), RP);
			break;
		case EMPTY:
			if (SHOW_PARSE_STEPS) {
				System.out.println("parse m_factor 2 EMPTY");
				System.out.println("mfactor " + lookAhead.getText());
			}
			currentExpression.addToken(lookAhead);
			accept(lookAhead.getText(), EMPTY);
		}
	}

	/**
	 * IDENTIFIER ;
	 */
	private void parse_m_var() {
		if (SHOW_PARSE_STEPS) {
			System.out.println("parse m_var");
		}
		
		accept(lookAhead.getText(), IDENTIFIER);
	}
	
	/**
	 * Checks if the token is already part of the Grammar, if not makes an new NonTerminal and adds it. </br>
	 * If it is already part of the Grammar, that NonTerminal will be returned.
	 * 
	 * @param {@link token} token
	 * @return [{@link NonTerminal} nonTerminal
	 */
	private NonTerminal getNonTerminal(EBNFToken token) {
		
		for(NonTerminal tempTerm : dataStructure){
			if( tempTerm.getText().equalsIgnoreCase( token.getText() ) ){
				return tempTerm;
			}
		}
		NonTerminal term = new NonTerminal(token);
		dataStructure.add(term);
		return term;
	}
	
	public ArrayList<NonTerminal> getDatastructure(){
		return dataStructure;
	}
	
	/**
	 * Generates a string of the Grammar.
	 */
	public String toString(){
		Boolean tempBool = false;
		String string = "";
		for(NonTerminal nonterm : dataStructure){
			if(nonterm.getKind() != 1337)
				string += nonterm.toString() + "\n";
			else
				tempBool = true;
		}
		if(tempBool == true)
			string += "\n";
		for(NonTerminal nonterm : dataStructure){
			if(nonterm.getKind() == 1337) 
				string += nonterm.toString() + "\n";
		}
		string += ".\n#";
		return string;
	}
	
}